#! /usr/bin/env bash
#
# Assembles a draft CHANGES entry out of revisions committed since the last
# entry was added. The entry is prepended to the current CHANGES file, and the
# user then gets a chance to further edit it in the editor before it gets
# committed.
#
# The script also maintains and updates a VERSION file.
#
# If the script finds a file called .update-changes.cfg it sources it at the
# beginning. That script can define a function "new_version_hook" that will be
# called with the new version number. It may use any of the replace_version_*
# functions defined below to update other files as necessary.
#
# If $1 is given, it's interpreted as a release version and a corresponding
# tag is created.

file_changes="CHANGES"              # The CHANGES file.
file_version="VERSION"              # The VERSION file.
file_config=".update-changes.cfg"   # This will be sourced if available.
new_version_hook="new_version_hook" # Function that will be called with new version number.
release_tag="release"               # We mark the current release with this tag.
beta_tag="beta"                     # We mark the current beta with this tag.
new_commit_msg="Updating CHANGES and VERSION." # Commit message when creating a new commit.
show_authors=1                      # Include author names with commit.

# The command line used to generate a revision's version string.
git_describe="git describe --always" # {rev} will be added.

# The command line used to generate a revision's date. The revision will be
# appended.  Not used with Zeek-style CHANGES file.
git_rev_date="git show -s --pretty=tformat:%ci"

# The command line used to generate the list of revisions between old and new
# state.
git_rev_list="git rev-list --topo-order HEAD" # ^{past-rev} will be added.

# The command line used to show the one-line summary of a revision before
# editing.
git_rev_summary="git show -s '--pretty=tformat: %h | %aN | %s'"  # {rev} will be added.

# The command line used to get a revision's author.
git_author="git show -s --pretty=format:%aN"  # {rev} will be added.
git_author_email="git show -s --pretty=format:%aE"  # {rev} will be added.

# The command line used to get a revision's message.
git_msg="git show -s --pretty=format:%B"  # {rev} will be added.

function usage
{
    echo "usage: `basename $0` [options]"
    echo
    echo "    -p <rev>    Explicitly name the past revision to compare with."
    echo "    -R <tag>    Tag the current revision as a release. Use VERSION to use that."
    echo "    -B <tag>    Tag the current revision as a beta release. Use VERSION to use that."
    echo "    -I          Initialize a new, initially empty CHANGES file."
    echo "    -c          Check whether CHANGES is up to date."
    echo
    exit 1
}

### Functions that can be used to replace version strings in other files.
### To use them, create a file $file_config and define a function
### "new_version_hook" in there that does whatever is necessary, like calling
### any of these.

# Function that looks for lines of the form 'VERSION="1.2.3"' in $1. It will
# replace the version number with $2 and then git-adds the change.
function replace_version_in_script
{
    file=$1
    version=$2

    cat $file | sed "s#^\\( *VERSION *= *\\)\"\\([0-9.-]\\{1,\\}\\)\"#\1\"$version\"#g" >$file.tmp
    cat $file.tmp >$file
    rm -f $file.tmp
    git add $file
}

# Function that looks for lines of the form '.. |version| replace:: 0.3' in $1.
# It will replace the version number with $2 and then git-adds the change.
function replace_version_in_rst
{
    file=$1
    version=$2

    cat $file | sed "s#^\\( *\.\. *|version| *replace:: *\\)\\([0-9a-zA-Z.-]\\{1,\\}\\)#\1$version#g" >$file.tmp
    cat $file.tmp >$file
    rm -f $file.tmp
    git add $file
}

# Function that looks for lines of the form 'version="1.2-3"' in $1. It will
# replace the version number with $2 and then git-adds the change.
function replace_version_in_setup_py
{
    file=$1
    version=$2

    cat $file | sed "s#\\([[:blank:]]*version[[:blank:]]*=[[:blank:]]*\\)\"\\([0-9.-]\\{1,\}\\)\"#\\1\"$version\"#g" >$file.tmp
    cat $file.tmp >$file
    rm -f $file.tmp
    git add $file
}

# Function that looks for lines of the form "#define .*VERSION "0.3"", with the
# number being "version * 100". It will replace the version with $2 and then
# git-adds the change.
function replace_version_in_c_header
{
    file=$1
    version=$2

    cat $file | sed "s#\\([[:blank:]]*\\#define[[:blank:]]*[_A-Za-z0-9]*_VERSION[[:blank:]]*\\)\"[0-9.-]\\{1,\\}\"#\\1\"$version\"#g" >$file.tmp
    mv $file.tmp $file
    git add $file
}

###

function version
{
    rev=$1
    $git_describe $rev --match "v*" 2>/dev/null | sed 's/^v//g' | sed 's/-g.*//g' | sed 's/-\([[:alnum:]]\{1,\}\)-\([0-9]\{1,\}\)$/-\1.\2/g'
}

function start_changes_entry
{
    version=$1
    dst=$2

    if [ "$zeek_style" == "0" ]; then
        date=`$git_rev_date HEAD`
        printf '%s | %s\n' "$version" "$date" >>$dst
    else
        date=`date`
        printf '%s %s\n' "$version" "$date" >>$dst
    fi
}

function add_to_changes_entry
{
   rev=$1
   dst=$2
   msg=$3

   author=""

   if [ "$msg" == "" ]; then
       author_email=`$git_author_email $rev`
       author=`$git_author $rev`

       if [[ "$author_email" == *@corelight.com ]]; then
           author="$author, Corelight"
       else
           author="$author"
       fi

       msg=`$git_msg $rev`
   fi

   if [ "$msg" == "" ]; then
       return 1
   fi

   if echo $msg | grep -q "^$new_commit_msg\$"; then
       # Ignore our own automated commits.
       return 1;
   fi

   echo >>$dst

   if [ "$zeek_style" == "0" ]; then
           bullet="  *"
       else
           bullet="-"
   fi

   ( echo -n "$msg"; test "$author" != "" && test "$show_authors" == "1" && printf " (%s)" "$author" ) \
       | awk -v bullet="$bullet" 'NR==1{printf "%s %s\n", bullet, $0; next }{printf "    %s\n", $0}' \
       | sed 's/[[:blank:]]*$//' >>$dst

   return 0;
}

function init_changes
{
   echo >>$file_changes
   start_changes_entry `version HEAD` $file_changes
   echo >>$file_changes
   echo "  * Starting $file_changes." >>$file_changes
}

function get_last_rev
{
    version=`cat $file_changes | egrep '^[0-9a-zA-Z.-]+  *\|' | head -1 | awk '{print $1}'`

    if echo $version | grep -q -- '-'; then
        # v1.0.4-14
        # Find the revision with that number.
        for rev in `git rev-list HEAD`; do
            v=`version $rev`

            if [ "$v" == "$version" ]; then
                echo $rev
                return
            fi
        done

        echo "Cannot determine revision for version $version." >/dev/stderr
        exit 1

    else
        # A tag.
        echo "v$version"
    fi
}

function check_release_tag
{
    if [ "$release" != "" ]; then
        git tag -d $release_tag 2>/dev/null
        git tag -a $release_tag -m "Current stable release."
        sleep 2 # Make sure git describe picks the next one.
        git tag -d $release 2>/dev/null
        git tag -a $release -m "Version tag"
        echo "Tagged with new tag $release and moved tag 'release' to here."
        echo
        echo "IMPORTANT: Don't forget to 'git push --tags'."
    fi
}

function check_beta_tag
{
    if [ "$beta" != "" ]; then
        git tag -d $beta_tag 2>/dev/null
        git tag -a $beta_tag -m "Current stable beta."
        sleep 2 # Make sure git describe picks the next one.
        git tag -d $beta 2>/dev/null
        git tag -a $beta -m "Beta version tag"
        echo "Tagged with new tag $beta and moved tag 'beta' to here."
        echo
        echo "IMPORTANT: Don't forget to 'git push --tags'."
    fi
}

function check_submodules
{
   if git submodule status --recursive | grep ^+; then
       cat <<EOF

The revision recorded for the module(s) above does not
match the one currently checked out in the respective
subdirs.

Please either update or checkout the recorded revision(s).

Aborting.
EOF

       exit 1
   fi
}

######

last_rev=""
release=""
beta=""
init=0
check=0
quiet=0

while getopts "hp:R:B:Ic" opt; do
    case "$opt" in
        p) last_rev="$OPTARG";;
        R) release="$OPTARG";;
        B) beta="$OPTARG";;
        I) init=1;;
        c) check=1; quiet=1;;
        *) usage;;
    esac
done

if [ -e $file_config ]; then
    if [ "$quiet" != "1" ]; then
        echo Reading $file_config ...
    fi
    source ./$file_config
fi


if [ "$release" != "" -a "$beta" != "" ]; then
    echo "Cannot tag as both beta and release."
    exit 1
fi

if [ "$release" == "VERSION" ]; then
    release="v`cat VERSION`"
fi

if [ "$beta" == "VERSION" ]; then
    beta="v`cat VERSION`"
fi

zeek_style=0        # If 1, we use a slightly different format.

if [ "$init" != "0" ]; then
    if [ -e $file_changes ]; then
        echo "$file_changes already exists, remove it first."
        exit 1
    else
        echo "Initializing $file_changes ..."
        init_changes
        exit 0
    fi
else
    if [ ! -e $file_changes ]; then
        echo "$file_changes does not exist, initialize it with '-I'."
        exit 1
    else
        # If we find this marker, it's Zeek-style CHANGES file.
        grep -vq -- '-+-+-+-+-+-+-+-+-+-' $file_changes;
        zeek_style=$?
    fi
fi

if [ "$release" != "" ]; then
    if ! echo $release | egrep -q '^v[0-9]+\.[0-9]+'; then
        echo "Release tag must be of the form vX.Y[.Z]"
        exit 1
    fi

    check_submodules
fi

if [ "$beta" != "" ]; then
    if ! echo $beta | egrep -q '^v[0-9]+\.[0-9]+(\.[0-9]+)?-(beta|rc)'; then
        echo "Release tag must be of the form vX.Y[.Z]-(beta|rc)*"
        exit 1
    fi

    check_submodules
fi

if [ "$last_rev" == "" ]; then
    last_rev=`get_last_rev`
fi

if [ "$last_rev" == "" ]; then
    echo 'Cannot determine previous revision to compare with, specify with "-p <rev>".'
    exit 1
fi

auto_version=`version HEAD`

if [ "$auto_version" == "" ]; then
    echo "Cannot determine version for HEAD did not return anything."
    exit 1
fi

tmp=${file_changes}.$$.tmp
trap "rm -f $tmp" EXIT
rm -f $tmp

found=0

echo >>$tmp

new_version=$auto_version
version=`version $rev`

if [ "$version" == "" ]; then
    echo "Cannot determine version for $rev."
    exit 1
fi

if [ "$release" != "" ]; then
    new_version=`echo $release | sed 's/v//g'`
fi

if [ "$beta" != "" ]; then
    new_version=`echo $beta | sed 's/v//g'`
fi

if [ "$quiet" != "1" ]; then
    echo "New version is $new_version."
    echo "Listing revisions commited since `version $last_rev` ($last_rev) ... "
    echo
fi

start_changes_entry $new_version $tmp

for rev in `$git_rev_list ^$last_rev`; do

   version=`version $rev`

   if [ "$version" == "" ]; then
        version="<no-version>"
   fi

   # printf "%15s |" $version

   if add_to_changes_entry $rev $tmp; then
       found=1

       if [ "$quiet" != "1" ]; then
           eval "$git_rev_summary $rev | grep -v '^$' | cat"
       fi
   fi

done

if [ "$found" == "0" ]; then
    if [ "$check" == "1" ]; then
        echo "CHANGES is up to date."
        exit 0
    fi

    echo "  None."
    echo

    if [ "$release" != "" -o "$beta" != "" ]; then
        add_to_changes_entry head $tmp "Release $new_version."
    else
        exit 0
    fi
fi

if [ "$check" == "1" ]; then
    echo "CHANGES is NOT to date."
    exit 0
fi

echo >>$tmp

cat $file_changes >>$tmp

# If we are ahead of origin, we can amend. If not, we need to create a new
# commit even if the user wants otherwise.
amend=0
if git remote | grep -q origin; then
     if git rev-list origin/`git rev-parse --abbrev-ref HEAD`..HEAD | grep -q .; then
         amend=1
     fi
fi

echo

if [ "$amend" == "0" ]; then
    echo Update to $file_changes will become a new commit.
else
    echo Update to $file_changes will be amended to last commit.
fi


echo
echo Type Enter to edit new $file_changes, or CTRL-C to abort without any modifications.
read

# Run editor.
if [ -z "$EDITOR" ]; then
    EDITOR=vi
fi
eval $EDITOR $tmp

# Put changes in place.
mv $tmp $file_changes
echo "Updated $file_changes."

if [ "$file_version" != "" ]; then
    echo $new_version >$file_version
    echo "Updated $version to $new_version."
fi

# Call hook function if it exists.
if type $new_version_hook >/dev/null 2>&1; then
    $new_version_hook $new_version
fi

# Commit changes.
git add $file_changes $file_version

if [ "$amend" == "1" ]; then
    git commit --amend
else
    git commit -m "$new_commit_msg"
fi

echo "Updates committed."

check_release_tag
check_beta_tag
